# Copyright (c) 2019 Presto Labs Pte. Ltd.
# Author: xguo

import datetime
import json
import logging
import time

import numpy as np
import recordclass

from coin.experimental.xguo.fruit.grape.price_graph import OrderSide
from coin.experimental.xguo.fruit.base.single_product import SingleProductPriceCalculator

from coin.exchange.bithumb.kr_rest.product_info import bithumb_tick_info
from coin.exchange.coinone_v2.kr_rest.product_info import coinone_tick_info
from coin.exchange.upbit_v1.kr_rest.product_info import upbit_tick_info


def to_hs(ts):
 if ts:
   return str(datetime.datetime.fromtimestamp(ts / 1e9))


def guess_tick_info(book):
  prices = book.get_ask_array(5) + book.get_bid_array(5)
  prices = np.sort(np.array([price[0] for price in prices]))
  tick = np.min(np.diff(prices))
  assert tick > 0
  return tick


def calculate_precision(num):
  precision = 0
  while abs(num - round(num)) > 1e-8:
    num *= 10
    precision += 1
    if precision > 100:
      raise ValueError()
  return precision


OrderFill = recordclass.recordclass(
    'OrderFill', ['timestamp', 'fill_price', 'fill_qty'])


class FillStats(object):
  def __init__(self, product, fee_bp=0):
    self._buy_qty = 0
    self._buy_amount = 0
    self._last_fill_buy_time = None

    self._sell_qty = 0
    self._sell_amount = 0
    self._last_fill_sell_time = None

    self._last_fill = None

    self._fee_bp = fee_bp
    self._price_calculator = SingleProductPriceCalculator(product)
    self._fill_buy_info = []
    self._fill_sell_info = []
    self._init_pos = None
    self._last_print_time = 0
    self._logger = logging.getLogger(__name__)

  @property
  def product(self):
    return self._price_calculator.product

  @property
  def buy_qty(self):
    return self._buy_qty

  @property
  def sell_qty(self):
    return self._sell_qty

  @property
  def position(self):
    return self._init_pos + self._buy_qty - self._sell_qty

  @property
  def imbalance(self):  # imbalance
    return self._buy_qty - self._sell_qty

  @property
  def imbalance_value(self):
    if self.imbalance > 0:
      imbalance_value = self.imbalance * self._price_calculator.bid0
    else:
      imbalance_value = self.imbalance * self._price_calculator.ask0
    return imbalance_value

  @property
  def avg_price(self):  # can be positive or negative
    return (self._buy_amount - self._sell_amount) / self.imbalance

  @property
  def ask0(self):
    return self._price_calculator.ask0

  @property
  def bid0(self):
    return self._price_calculator.bid0

  @property
  def total_volume(self):
    return self._sell_amount + self._buy_amount

  @property
  def timestamp(self):
    return self._price_calculator.timestamp

  @property
  def total_fee(self):
    return self.total_volume * self._fee_bp * 1e-4

  @property
  def pnl(self):
    return self._sell_amount - self._buy_amount + self.imbalance_value - self.total_fee

  def is_initialized(self):
    return self._init_pos is not None

  def set_init_pos(self, pos):
    self._logger.info("set_init_pos called! %s, %s", self.product, pos)
    self._init_pos = pos

  def get_lowest_fill_buy_price(self):
    if self._fill_buy_info:  # big -> small
      return self._fill_buy_info[-1].fill_price

  def get_highest_fill_buy_price(self):
    if self._fill_buy_info:  # big -> small
      return self._fill_buy_info[0].fill_price

  def get_lowest_fill_sell_price(self):
    if self._fill_sell_info:  # small -> big
      return self._fill_sell_info[0].fill_price

  def get_highest_fill_sell_price(self):
    if self._fill_sell_info:  # small -> big
      return self._fill_sell_info[-1].fill_price

  def reset_fill_info(self):
    self.print('Reset buy/sell fill info, PNL from fill')
    self._fill_buy_info = []
    self._fill_sell_info = []

  def get_last_fill_buy_time(self):
    return self._last_fill_buy_time

  def get_last_fill_sell_time(self):
    return self._last_fill_sell_time

  def get_last_fill_time(self):
    sell_time = self.get_last_fill_sell_time()
    buy_time = self.get_last_fill_buy_time()

    if (sell_time is None) and (buy_time is None):
      return None
    else:
      return max(sell_time or 0, buy_time or 0)

  def get_last_fill_elapsed_time(self):
    ts = self.get_last_fill_time()
    if ts is not None:
      delta_ts = time.time() * 1e9 - ts
      return delta_ts

  def get_price_list(self):
    res = {'buy': [], 'sell': []}
    for fill in self._fill_buy_info:
      res['buy'].append('%s@%s, %s' % (fill.fill_qty, fill.fill_price, to_hs(fill.timestamp)))
    for fill in self._fill_sell_info:
      res['sell'].append('%s@%s, %s' % (fill.fill_qty, fill.fill_price, to_hs(fill.timestamp)))
    return res

  def update(self, fill_price, fill_qty, order):
    ts = time.time() * 1e9
    self.update_sim(fill_price, fill_qty, order.order_side)

    if order.order_side == OrderSide.BUY:
      self._last_fill_buy_time = ts
    else:
      self._last_fill_sell_time = ts
    self.print()

  def update_sim(self, fill_price, fill_qty, order_side):
    order_fill = OrderFill(
        timestamp=self.timestamp,
        fill_price=fill_price, fill_qty=fill_qty)

    self._last_fill = order_fill
    if order_side == OrderSide.BUY:
      self._buy_qty += fill_qty
      self._buy_amount += (fill_qty * fill_price)
      self.match_fill(fill_price, fill_qty, order_side)
    elif order_side == OrderSide.SELL:
      self._sell_qty += fill_qty
      self._sell_amount += (fill_qty * fill_price)
      self.match_fill(fill_price, fill_qty, order_side)
    else:
      raise ValueError('Invalid order side!')

  def update_book(self, book):
    self._price_calculator.update(book)
    if self.timestamp - self._last_print_time > 1200 * 1e9:
      self.print()

  def match_fill(self, fill_price, fill_qty, order_side):
    # choose queue
    if order_side == OrderSide.BUY:
      queue = self._fill_sell_info  # oppsite side
      sign = 1
    else:
      queue = self._fill_buy_info
      sign = -1

    # do match
    remove_list = []
    for fill in queue:
      if (fill.fill_price - fill_price) * sign > self._fee_bp * 1e-4:
        if fill_qty <= fill.fill_qty:
          fill.fill_qty -= fill_qty
          fill_qty = 0
          if fill.fill_qty < 1e-8:
            remove_list.append(fill)
          break
        else:
          remove_list.append(fill)
          fill_qty -= fill.fill_qty

    # remove matched fills
    for rm_fill in remove_list:
      queue.remove(rm_fill)

    # append new fill
    if fill_qty > 1e-8:
      fill = OrderFill(timestamp=self.timestamp, fill_price=fill_price, fill_qty=fill_qty)
      if sign > 0:
        self._fill_buy_info.append(fill)  # big -> small
        self._fill_buy_info.sort(key=lambda x: x.fill_price, reverse=True)
      else:
        self._fill_sell_info.append(fill)  # small -> big
        self._fill_sell_info.sort(key=lambda x: x.fill_price, reverse=False)

  def to_dict(self):
    return {
        'symbol': self.product.full_symbol,
        # buy
        'buy_qty': self._buy_qty,
        'buy_amount': self._buy_amount,
        'last_fill_buy_time': to_hs(self.get_last_fill_buy_time()),
        'avg_buy_price': self._buy_amount / (self._buy_qty + 1e-8),
        #sell
        'sell_qty': self._sell_qty,
        'sell_amount': self._sell_amount,
        'last_fill_sell_time': to_hs(self.get_last_fill_sell_time()),
        'avg_sell_price': self._sell_amount / (self._sell_qty + 1e-8),
        # other
        'fee_bp': self._fee_bp,
        'position': self.position,
        'imbalance': self.imbalance,
        'imbalance_value': self.imbalance_value,
        'estimated_pnl_without_fee': self.pnl + self.total_fee,
        'estimated_pnl': self.pnl,
        'total_volume': self.total_volume,
        'return_bp': self.pnl / (self.total_volume + 1e-8),
        'ask0': self.ask0,
        'bid0': self.bid0,
        'price_list': self.get_price_list(),
    }

  def print(self, msg=None):
    msg = msg or 'PNL from fill'
    self._last_print_time = self.timestamp
    self._logger.info('%s:\n%s', msg, json.dumps(self.to_dict(), indent=2))


class LatestFeed(object):
  def __init__(self):
    self._books = {}
    self._ts = 0
    self._logger = logging.getLogger(__name__)

  @property
  def timestamp(self):
    return self._ts

  def update_book(self, book):
    if isinstance(book, dict):
      timestamp = book['timestamp']
    else:
      timestamp = book.timestamp

    if timestamp < self._ts:
      self._logger.error('Reversed timestamp! %s, %s', timestamp, self._ts)
      return
    self._ts = timestamp

    if isinstance(book, dict):
      self._books[book['product']] = book
    else:
      self._books[book.product] = book

  def get_book(self, product):
    return self._books.get(product)

  def is_available(self, *products):
    return all(self._books.get(product) for product in products)
